#BuildVCProject.py


#1. get preject name
#2. copy template to dest
#3. update project name in .project, config.mk, depends.mk


import sys,os
import shlex
import CDTProjectFile

from xml.etree.ElementTree import ElementTree
import re


class VcProject:
    version='9.00'
    appmacro = {}
    appmacro['Platform'] = 'Win32';
    appmacro['ConfigurationName'] = 'Debug';
    appmacro['InputDir'] = os.getcwd();
    appmacro['OutDir'] = '$(ConfigurationName)';
    appmacro['IntDir'] = '$(ConfigurationName)';
    appmacro['ProjectName'] = 'Template';
    appmacro['ProjectDir'] = os.getcwd();
    appmacro['SolutionName'] = '';
    appmacro['SolutionDir'] = '';
    appmacro['TargetFileName'] = '$(TargetName).exe';
    appmacro['TargetName'] = 'Template';
    appmacro['precompiledHeader'] = 'stdafx.h';
    
    Files={}
    Configuration={}
    Configuration['global']={};
    Configuration['VCCLCompilerTool']={};
    Configuration['VCLinkerTool']={};
    
    #created from config file
    resourceFile= None;
    
    #the obj files and res file created by cl.exe and rc.exe
    outputObjs = set();
    
    def __init__(self,projectFile,configurationName):
        self.appmacro['ConfigurationName'] = configurationName.lower().capitalize();
        self.parseFile(projectFile);
#        print("parse file completed: ", projectFile)
    def parseFile(self,file):
        vcConfig = ElementTree();
        #retry 5 times if failed
        for i in range(5):
            try:
                vcConfig.parse(file);
                break;
            except :
#                print('some error while parsing file, retry...')
                pass
        if(not vcConfig.getroot()):
            raise IOError("can't parse vc project file,if it is valid project file, try to change xml encoding to utf-8");
        root = vcConfig.getroot();
        assert root.tag == 'VisualStudioProject';
        assert root.get('ProjectType') == 'Visual C++';
        
        appmacro = self.appmacro;
        appmacro['ProjectName'] =root.get('Name');
        appmacro['TargetName']  =root.get('Name');
        appmacro['Platform']    =root.find('Platforms/Platform').get('Name');
        
        configs = root.findall('Configurations/Configuration');
        selectConfig = None;
        selectConfiguration = self.appmacro['ConfigurationName'] +'|'+self.appmacro['Platform'];
        for config in configs:
            if(config.get('Name') == selectConfiguration ):
                selectConfig = config;
                break;
        if(selectConfig is None):
            raise ValueError(selectConfiguration + 'is not a valid configuration');
        
        self.Configuration['global'].update(selectConfig.attrib);
        tools = selectConfig.getchildren();
        toolsMap = {}
        for tool in tools:
            if(tool.tag == 'Tool'):
                toolsMap[tool.get('Name')] = tool;

        baseCfg = self.Configuration['global'];
        self.Configuration['VCCLCompilerTool'] = {};
        
        self.Configuration['VCCLCompilerTool']['ExceptionHandling'] = '1';
        self.Configuration['VCCLCompilerTool'].update(baseCfg.copy());
        self.Configuration['VCCLCompilerTool'].update(toolsMap['VCCLCompilerTool'].items());
        
        self.Configuration['VCLinkerTool'] = {}; 
        linkConfig = self.Configuration['VCLinkerTool'];
        #set default property here
        linkConfig['OutputFile'] = '$(OutDir)\$(ProjectName).exe';
        linkConfig.update(baseCfg);
        linkConfig.update(toolsMap['VCLinkerTool'].items());
        
        if(linkConfig.get('SubSystem') == '2' ):
            if(linkConfig.get('EntryPointSymbol') == None):
                if(linkConfig.get('CharacterSet') == '1'):
                    linkConfig['EntryPointSymbol'] = 'wWinMainCRTStartup';
                else:
                    linkConfig['EntryPointSymbol'] = 'WinMainCRTStartup';
        
        self.Configuration['VCResourceCompiler'] = baseCfg.copy();
        self.Configuration['VCResourceCompiler'].update(toolsMap['VCResourceCompilerTool'].items());
        
        filters =root.findall('Files/Filter');
        for filter in filters:
            key = filter.get('Name');
            files = filter.getchildren();
            lst = [];
            for f in files:
                lst.append(f);
            self.Files[key] = lst;
        
        globalvalues = root.findall('Globals/Global');
        for v in globalvalues:
            if(v.get('Name') == 'RESOURCE_FILE'):
                self.resourceFile = v.get('Value');
    def compileCpp(self):
        toBeCompiledCppByCmdLine = {};
        generalCmdLine = self.genCLCmdFromConfig(self.Configuration['VCCLCompilerTool']);
    
        cpplist = self.Files['Source Files'];
        objfiles = []
        for cpp in cpplist:
            objfiles.append(os.path.splitext( cpp.get('RelativePath'))[0]+'.obj');
            if(not cpp.findall('FileConfiguration')):
                toBeCompiledCppByCmdLine.setdefault(generalCmdLine,[]).append(cpp.get('RelativePath'));
            else:
                toolsMap = self.__getToolsConfigInMultiConfig(cpp.findall('FileConfiguration'));
                clConfig = dict(self.Configuration['VCCLCompilerTool']);
                clConfig.update(toolsMap.get('VCCLCompilerTool').attrib or {});
                fileCmdLine = self.genCLCmdFromConfig(clConfig);
                toBeCompiledCppByCmdLine.setdefault(fileCmdLine,[]).append(cpp.get('RelativePath'));
                pass
        cmdlines = self.preProcessCompileCpp(toBeCompiledCppByCmdLine);
        for it in cmdlines:
            cmd =it[0] + ' ' + ' '.join(it[1]);
            print(cmd);
            res = os.system(cmd);
            if(res != 0):return res;
            #record all the result obj files 
            outdir = shlex.split(cmd[cmd.index('/Fo')+3:])[0];
            for obj in objfiles:
                self.outputObjs.add(outdir+obj);
        return 0;
    def preProcessCompileCpp(self,cmdDict):
        cmdlines = [];
        for cmd in cmdDict:
            if(' /Yc' in cmd):
                precompiledHeader = shlex.split(cmd[cmd.index(' /Fp')+4:])[0];
                dir = os.path.dirname( precompiledHeader);
                if (not os.path.isdir(dir)): os.makedirs( os.path.dirname( precompiledHeader));
                cmdlines.insert(0, (cmd,cmdDict[cmd]));
            else:
                cmdlines.append((cmd,cmdDict[cmd]));
        return cmdlines;
    def compileRes(self):
        if(self.resourceFile is None):
            return 0;
        cmdline = self.__genResCmdFromConfig(self.Configuration['VCResourceCompiler']);
        cmd = cmdline + ' ./' + self.resourceFile;
        print(cmd);
        res = os.system(cmd);
        self.outputObjs.add(shlex.split(cmd[cmd.index('/fo')+3:])[0]);
        return res;
    def linkObjs(self):
        cmdline = self.__genLinkCmdFromConfig(self.Configuration['VCLinkerTool']);
        cmdline = cmdline+' ';
        for obj in self.outputObjs:
            cmdline = cmdline + ' "' + obj.replace('"','') + '"';
        cmdline = cmdline.replace('\\.\\','\\');
        print(cmdline);
        os.system(cmdline);
        pass
    def __genLinkCmdFromConfig(self,linkConfig):
        cmdLine = '';
        for o in linkConfig.items():
            cmdLine = cmdLine +" " + self.__translateLinkValue(o);
        cmdLine = 'link.exe ' + cmdLine +' /nologo /errorReport:prompt ';
        cmdLine=self.__substituteMacro(cmdLine);
        return cmdLine;
    def genCLCmdFromConfig(self,clConfig):
        cmdLine = '';
        for o in clConfig.items():
            cmdLine = cmdLine +" " + self.__translateCLValue(o);
        cmdLine= cmdLine + r' /Fp"$(IntDir)\$(TargetName).pch" /Fo"$(IntDir)\\" /Fd"$(IntDir)\vc90.pdb"' + ' /c /TP ';
        cmdLine = 'cl.exe ' + cmdLine +' /nologo /errorReport:prompt';
        cmdLine=self.__substituteMacro(cmdLine);
        return cmdLine;
    def __genResCmdFromConfig(self,clConfig):
        cmdLine = '';
        for o in clConfig.items():
            cmdLine = cmdLine +" " + self.__translateResValue(o); 
        cmdLine= cmdLine + r' /I "$(IntDir)" /fo"$(IntDir)/$(TargetName).res"';
        cmdLine = 'rc.exe ' + cmdLine;
        cmdLine=self.__substituteMacro(cmdLine);
        return cmdLine;
    def __getToolsConfigInMultiConfig(self,configs):
        selectConfigName = self.appmacro['ConfigurationName'] +'|'+self.appmacro['Platform'];
        tools = [];
        for cfg in configs:
            if(cfg.get('Name') == selectConfigName):
                tools=cfg.getchildren();
                break;
        toolsMap = {}
        for tool in tools:
            if(tool.tag == 'Tool'):
                toolsMap[tool.get('Name')] = tool;
        return toolsMap;
    def __substituteMacro(self,line):
        matchcount = -1;
        totaltimes = 0;
        while(matchcount != 0 and totaltimes < 1000):
            matchcount = 0;
            totaltimes +=1;
            for (k,v) in self.appmacro.items():
                if('$('+k+')' in line):
                    matchcount +=1;
                    line = line.replace('$('+k+')', v);
        
        
        p = re.compile(r'\$\([^\)]*\)');
        if(p.search(line) is not None ):
            line = p.sub('',line);
        return line;
    def __translateCLValue(self,option):
        k=option[0];
        v=option[1];
        if(k == 'CharacterSet'):
            return {'0':'','1':'/D "_UNICODE" /D "UNICODE"','2':''}.get(v) or '';
        
        if(k == 'UseOfMFC'):
            return {'0':'','1':'','2':'/D "_AFXDLL"'}.get(v) or '';
        
        if(k == 'Optimization'):
            return {'0':'/Od','1':'/O1','2':'/O2'}.get(v) or '';
        
        if(k == 'AdditionalIncludeDirectories'):
            v = v.replace(r'&quot;', '"');
            paths = v.split(';');
            return '/I '+ ' /I '.join(paths);
        
        if(k == 'PreprocessorDefinitions'):
            defs = v.split(';');
            return '/D "' + '" /D "'.join(defs) + '"';
        
        if(k == 'MinimalRebuild'):
            if(v):return '/Gm';
            else:return '';
            
        if(k == 'ExceptionHandling'):
            return {'0':'','1':'/EHsc','2':'/EHa'}.get(v) or '';
        
        if(k == 'BasicRuntimeChecks'):
            return {'0':'','1':'/RTCs','2':'/RTCu','3':'/RTC1'}.get(v) or '';
        
        if(k == 'RuntimeLibrary'):
            return {'0':'/MT','1':'/MTd','2':'/MD','3':'/MDd'}.get(v) or '';
        
        if(k == 'UsePrecompiledHeader'):
            return {'0':'','1':'/Yc"$(precompiledHeader)"','2':'/Yu"$(precompiledHeader)"'}.get(v) or '';
        
        if(k == 'WarningLevel'):
            return {'0':'','1':'/W1','2':'/W2','3':'/W3','4':'/W4'}.get(v) or '';
        
        if(k == 'DebugInformationFormat'):
            return {'0':'','1':'/Z7','2':'','3':'/Zi','4':'/ZI'}.get(v) or '';

        return ''
    def __translateResValue(self,option):
        k=option[0];
        v=option[1];
        if(k == 'CharacterSet'):
            return {'0':'','1':'/d "_UNICODE" /d "UNICODE"','2':''}.get(v) or '';
        
        if(k == 'UseOfMFC'):
            return {'0':'','1':'','2':'/D "_AFXDLL"'}.get(v) or '';
        
        if(k == 'PreprocessorDefinitions'):
            defs = v.split(';');
            return '/d "' + '" /d "'.join(defs) + '"';
        
        if(k == 'Culture'):
            return '/l '+hex(int(v));
        
        if(k == 'AdditionalIncludeDirectories'):
            v = v.replace(r'&quot;', '"');
            paths = v.split(';');
            return '/I '+ ' /I '.join(paths);
        
        if(k == 'ResourceOutputFileName'):
            return ' /fo' + v;
        return '';
    def __translateLinkValue(self,option):
        k=option[0];
        v=option[1];
        if(k == 'EntryPointSymbol'):
            return ' /ENTRY:"' + v.replace('"','') + '"';
        if(k == 'OutputFile'):
            return ' /OUT:"' + v.replace('"','') + '"';
    
        if(k == 'LinkIncremental'):
            return {'0':'','1':'/INCREMENTAL:NO','2':'/INCREMENTAL'}.get(v) or '';
        
        if(k == 'AdditionalDependencies'):
            return v;
        
        if(k == 'AdditionalLibraryDirectories'):
            v = v.replace(r'&quot;', '');
            paths = v.split(';');
            return '/LIBPATH:"'+ ' /LIBPATH:"'.join(paths) + '"';
        
        if(k == 'GenerateDebugInformation'):
            if(v):return '/DEBUG';
            else:return '';
            
        if(k == 'SubSystem'):
            return {'0':'',
                    '1':'/SUBSYSTEM:CONSOLE',
                    '2':'/SUBSYSTEM:WINDOWS',
                    '3':'/SUBSYSTEM:NATIVE',
                    '4':'/SUBSYSTEM:EFI_APPLICATION',
                    '5':'/SUBSYSTEM:EFI_BOOT_SERVICE_DRIVER',
                    '6':'/SUBSYSTEM:EFI_ROM',
                    '7':'/SUBSYSTEM:EFI_RUNTIME_DRIVER',
                    '8':'/SUBSYSTEM:WINDOWSCE'}.get(v) or '';
            
        if(k == 'TargetMachine'):
            return {'0':'',
                    '1':'/MACHINE:X86',
                    '2':'/MACHINE:AM33',
                    '3':'/MACHINE:ARM',
                    '4':'/MACHINE:EBC',
                    '5':'/MACHINE:IA64',
                    '6':'/MACHINE:M32R',
                    '7':'/MACHINE:MIPS',
                    '17':'/MACHINE:X64'}.get(v) or '';
        return '';
    def getMacroDefines(self):
        defines = [];
        clCmdLine = self.genCLCmdFromConfig(self.Configuration['VCCLCompilerTool']);
        options = clCmdLine.split();
        for i in range(len(options)):
            if(options[i].lower() == '/d'):
                defines.append(options[i+1].replace('"',''));
        return defines;
    def getAdditionalIncludes(self):
        additionalIncludes = [];
        clCmdLine = self.genCLCmdFromConfig(self.Configuration['VCCLCompilerTool']);
        options = clCmdLine.split();
        for i in range(len(options)):
            if(options[i].lower() == '/i'):
                additionalIncludes.append(options[i+1].replace('"',''));
        return additionalIncludes;
    
def doHelp():
    filename = os.path.basename(sys.argv[0]);
    print('Build vc project, be used by cdt via makefile, simulate the command in vc.')
    print('the vc project should have been created by vc before use this tool.');
    print('');
    print(filename,' Argument');
    print('Argument List:');
    print('build [release]', '\tbuild project');
    print('rebuild [release]', '\trebuild project');
    print('configcdt [release]', '\tconfig cdt project');
    print('clean ', '\tclean project');
    print('');
    print('Examples:');
    print(filename,' build debug');
    
def doBuild(configurationName):
    files = os.listdir(os.getcwd());
    proj = None;
    for f in files:
        if(os.path.isfile(f) and f.endswith('.vcproj')):
            proj = VcProject(f,configurationName);
    if(proj is None):
        print('vc project file(*.vcproj) not found.');
        return 0;
    else:
        res = 0;
        res = proj.compileCpp();
        if(res != 0):return res;
        res = proj.compileRes();
        if(res != 0):return res;
        return proj.linkObjs();
    pass
def doRebuild(configurationName):
    doBuild(configurationName);
    pass
def doClean():
    command = ['del /S *.obj *.res']; 
    os.system(command);
    pass
def doConfigCDT(configurationName):
    if(os.path.isfile('Makefile')):
        print('makefile already exist, if you continue, this makefile will be replaced with new generated file.',
              'project specific files will also be replaced if they are exist.');
        if(input('continue(y/n):').lower() == 'y'):
            pass
        else:
            return

    files = os.listdir(os.getcwd());
    proj = None;
    for f in files:
        if(os.path.isfile(f) and f.endswith('.vcproj')):
            proj = VcProject(f,configurationName);
    if(proj is None):
        print('vc project file(*.vcproj) not found.');
        return;
    
    projFile = CDTProjectFile.CDTProjectFile();
    defines = proj.getMacroDefines();
    additionalIncludes = proj.getAdditionalIncludes();
    buildInIncludes = os.getenv('INCLUDE','').strip().strip(';').split(';');
    
    projFile.includeLibPaths = os.getenv('LIB','').strip().strip(';').split(';');
    projFile.includeLibs = []; 
    projFile.defines = defines;
    projFile.includes = list(set(buildInIncludes + additionalIncludes));
    projFile.projectName = proj.appmacro['ProjectName']
    
    with open('.project',mode='w') as file:
        file.write(projFile.generateProject());
        
    with open('.cproject',mode='w') as file:
        file.write(projFile.generateCProject());
    
    with open('Makefile',mode='w') as file:
        file.write(projFile.generateMakefile());
    print('configuration for cdt project complete.')
    pass
def main():
    if len(sys.argv) < 2:
        doHelp();
        return;
    elif(len(sys.argv) == 2):
        sys.argv.append('Debug');
    
    if(sys.argv[1] == 'configcdt'):
        doConfigCDT(sys.argv[2]);
    elif(sys.argv[1] == 'build'):
        doBuild(sys.argv[2]);
    elif(sys.argv[1] == 'rebuild'):
        doRebuild(sys.argv[2]);
    elif(sys.argv[1] == 'clean'):
        doClean();
    else:
        doHelp();
if __name__ == '__main__':
    main();
